const { getMappedPath, isFunction, setProp } = require("../../lib/helper");
const { MODULEPATH } = require("../../lib/const");
const { CacheStore } = require('./../../lib/store');
const Queue = require('./../../lib/queue');
const Env = require('./../env');

const EmptyPersistence = {
    manifest: {},
    data: {},
    getAll() {
        return Promise.resolve(this.data);
    },
    saveAll(data) {
        Object.assign(this.data, data || {});
        return Promise.resolve();
    },
    clean(apps) {
        return Promise.resolve();
    },
    getManifest(name, version) {
        let t = this.manifest[name];
        if (t && version && t.version === version) {
            return Promise.resolve(t.data);
        } else {
            return Promise.resolve(null);
        }
    },
    setManifest(data) {
        let { name, version } = data;
        this.manifest[name] = { version, data };
        return Promise.resolve();
    }
}

const StoragePersistence = {
    getAll() {
        let r = {};
        try {
            r = window.localStorage.getItem("ada-local-source");
        } catch (e) {
        }
        return Promise.resolve(r);
    },
    saveAll(data) {
        try {
            window.localStorage.setItem("ada-local-source", JSON.stringify(data));
        } catch (e) {
        }
        return Promise.resolve();
    },
    clean(apps) {
        try {
            return this.getAllManifest().then(data => {
                let rManifest = {};
                Reflect.ownKeys(data).forEach(key => {
                    let info = data[key];
                    let r = apps.find(a => a.name === key && a.version === info.version);
                    if (r) {
                        rManifest[key] = info;
                    }
                });
                let keys = [];
                Reflect.ownKeys(rManifest).forEach(name => {
                    let { data } = rManifest[name];
                    let { map } = data;
                    keys = keys.concat(Reflect.ownKeys(map).filter(a => a !== 'packages').map(a => map[a]));
                });
                return { keys, manifest: rManifest };
            }).then(({ keys, manifest }) => {
                return this.getAll().then(data => {
                    let r = {};
                    Reflect.ownKeys(data).forEach(a => {
                        if (keys.includes(a)) {
                            r[a] = data[a];
                        }
                    });
                    return { source: r, manifest };
                });
            }).then(({ source, manifest }) => {
                return this.saveAll(source).then(() => this.setAllManifest(manifest));
            });
        } catch (_) {
        }
        return Promise.resolve();
    },
    getAllManifest() {
        let index = 0, r = {};
        while (true) {
            let a = window.localStorage.key(index);
            if (a) {
                let b = 'ada-local-manifest-';
                if (a.indexOf(b) === 0) {
                    let name = a.substring(b.length);
                    r[name] = JSON.parse(window.localStorage.getItem(a));
                }
                index++;
            } else {
                break;
            }
        }
        return Promise.resolve(r);
    },
    setAllManifest(data) {
        Reflect.ownKeys(data).forEach(name => {
            window.localStorage.setItem(`ada-local-manifest-${name}`, JSON.stringify(data[name]));
        });
        return Promise.resolve();
    },
    getManifest(name, version) {
        let t = window.localStorage.getItem(`ada-local-manifest-${name}`);
        if (t && version) {
            try {
                t = JSON.parse(t);
                if (t.version === version) {
                    return Promise.resolve(t.data);
                }
            } catch (_) { }
        }
        return Promise.resolve(null);
    },
    setManifest(data) {
        let { name, version } = data;
        window.localStorage.setItem(`ada-local-manifest-${name}`, JSON.stringify({ version, data }));
        return Promise.resolve();
    }
}

const DatabasePersistence = {
    sourceStore: null,
    manifestStore: null,
    ready() {
        !this.sourceStore && (this.sourceStore = new CacheStore({ name: 'ada-assets-source', store: 'source' }));
        !this.manifestStore && (this.manifestStore = new CacheStore({ name: 'ada-assets-manifest', store: 'manifest' }));
    },
    getAll() {
        return this.sourceStore.getAll();
    },
    saveAll(data) {
        return this.sourceStore.setAll(data);
    },
    clean(apps) {
        return this.getAllManifest().then(data => {
            let rManifest = {}, keys = [];
            Reflect.ownKeys(data).forEach(key => {
                let info = data[key];
                let r = apps.find(a => {
                    return a.name === key && a.version === info.version;
                });
                if (r) {
                    rManifest[key] = r;
                }
            });
            Reflect.ownKeys(rManifest).forEach(name => {
                let data = rManifest[name];
                let { map = {} } = data.codeMap || {};
                keys = keys.concat(Reflect.ownKeys(map).filter(a => a !== 'packages').map(a => map[a]));
            });
            return { keys, manifest: rManifest };
        }).then(({ keys, manifest }) => {
            return this.getAll().then(data => {
                let r = {};
                Reflect.ownKeys(data).forEach(a => {
                    if (keys.includes(a)) {
                        r[a] = data[a];
                    }
                });
                return { source: r, manifest };
            });
        }).then(({ source, manifest }) => {
            return this.saveAll(source).then(() => this.setAllManifest(manifest));
        });
    },
    getAllManifest() {
        return this.manifestStore.getAll();
    },
    setAllManifest(data) {
        return this.manifestStore.setAll(data, (key, data, cache) => {
            if (cache[key] !== undefined) {
                return cache[key].version !== data.version;
            } else {
                return true;
            }
        });
    },
    getManifest(name, version) {
        return this.getAllManifest().then(t => {
            if (t[name] && t[name].version === version) {
                return t[name];
            } else {
                return null;
            }
        });
    },
    setManifest(data) {
        return this.manifestStore.setAll(data, (key, data, cache) => {
            if (cache[key] !== undefined) {
                return cache[key].version !== data.version;
            } else {
                return true;
            }
        });
    }
}

const CommonInstalled = new Map();
const ManifestLoaderMap = new Map();

class InstalledManager {
    constructor() {
        this._common = Env.commonModules;
        this._installed = new Map();
    }

    isCommon(path) {
        return path.indexOf('node_modules/') === 0;
    }

    has(path) {
        return this._common.has(path) || this._installed.has(path);
    }

    get(path) {
        return this._common.get(path) || this._installed.get(path);
    }

    set(path, info) {
        if (this.isCommon(path)) {
            this._common.set(path, info);
        } else {
            this._installed.set(path, info);
        }
    }

    delete(path) {
        if (this.isCommon(path)) {
            this._common.delete(path);
        } else {
            this._installed.delete(path);
        }
    }

    scan(fn) {
        [this._common, this._installed].some(map => {
            let ite = map.entries(), _r = false;
            while (true) {
                let { value, done } = ite.next();
                if (done) {
                    break;
                } else {
                    let [key, val] = value;
                    let r = fn(key, val);
                    if (r === false) {
                        _r = true;
                        break;
                    }
                }
            }
            return _r;
        });
    }
}

class ModuleManager {
    constructor(context, loader) {
        this.installed = new InstalledManager();
        this.moduleFnsLoaded = new Map();
        this._loader = loader;
        this._context = context;
    }

    getExcutor(path, code) {
        let suffix = path.split("/").pop().split(".").pop();
        let source = code;
        let id = this._context.name + '-' + path.replace(/\//g, "-");
        if (suffix === "js") {
            source = code;
        } else if (suffix === "css" || suffix === "scss" || suffix === "less") {
            source = `module.exports={
    style:${JSON.stringify(code)},
    active:function(){var styles={_linkElement:null,active:function(){var et=document.getElementById("${id}");if(!et){var _a = document.createElement("style");_a.setAttribute("media", "screen");_a.setAttribute("type", "text/css");_a.setAttribute("id","${id}");_a.appendChild(document.createTextNode(${JSON.stringify(code)}));styles._linkElement=_a;document.getElementsByTagName("head")[0].appendChild(_a);}else{et.innerText="";et.appendChild(document.createTextNode(${JSON.stringify(code)}));}}};if(/complete|loaded|interactive/.test(window.document.readyState)){styles.active();}else{window.document.addEventListener('DOMContentLoaded', function(){styles.active();},false);}},
    isReady:function(){return styles._linkElement!==null;},
    getElement:function(){return styles._linkElement;},
    remove:function(){if(styles._linkElement){styles._linkElement.parentNode.removeChild(styles._linkElement);}}};`;
        } else if (suffix === "json") {
            source = `module.exports=${code}`;
        } else if (suffix === "html") {
            source = `module.exports={template:${JSON.stringify(code)}}`;
        } else if (suffix === 'text') {
            source = `module.exports=${JSON.stringify(code)}`;
        } else {
            source = code;
        }
        let info = { path, code: source };
        try {
            return new Function("module", "exports", "require", "imports", "window", "document", info.code);
        } catch (e) {
            console.log(path);
            console.log(info.code);
            console.log(e);
            throw e;
        }
    }

    getModuleDependenceInfo(_, code) {
        let paths = [];
        code = code.replace(/require\(.*?\)/g, (one) => {
            let _path = one.substring(8, one.length - 1).replace(/['|"|`]/g, "").trim();
            paths.push(_path);
            return `require("${_path}")`;
        });
        return { code, paths };
    }

    getModuleDependenceMap(path) {
        return this._loader.get(path).then(source => {
            let { paths, code: moduleCode } = this.getModuleDependenceInfo(path, source);
            let result = { [path]: moduleCode }, works = [];
            paths.forEach(path => {
                !this.installed.has(path) && works.push(this.getModuleDependenceMap(path));
            });
            return Promise.all(works).then(maps => Object.assign(result, ...maps));
        });
    }

    excute(path) {
        if (this.installed.has(path)) {
            return this.installed.get(path).exports;
        }
        let module = { path: path, exports: {} };
        this.installed.set(path, module);
        this.moduleFnsLoaded.get(path).call(module.exports, module, module.exports, (path) => this.excute(path), (path) => {
            return this.load(path).then(_result => _result.__esModule ? _result.default : _result).then(result => {
                isFunction(result) && setProp(result, MODULEPATH, path);
                return result;
            });
        }, this._context.window, this._context.document);
        return module.exports;
    }

    load(path) {
        let current = this.installed.get(path);
        if (current) {
            return Promise.resolve(current.exports);
        } else {
            return this.getModuleDependenceMap(path).then(moduleMap => {
                let count = 0;
                return Reflect.ownKeys(moduleMap).reduce((a, i) => {
                    count++;
                    return a.then(() => {
                        let info = { name: i, code: moduleMap[i] };
                        return this._context._invokeHook('onsourceready', info).then(() => info.code);
                    }).then((code) => {
                        this.moduleFnsLoaded.set(i, this.getExcutor(i, code));
                    });
                }, Promise.resolve()).then(() => {
                    if (count > 50) {
                        console.warn(`[ada] entry[ ${path} ] has too many dependens,use async import`);
                    }
                    return this.excute(path);
                });
            });
        }
    }

    scan(fn) {
        fn && this.installed.scan((i, val) => fn(i, val.exports));
    }

    scanClass(fn) {
        if (fn) {
            this.installed.scan((i, a) => {
                if (!a.exports.__esModule && isFunction(a.exports)) {
                    return fn(i, a.exports);
                }
                for (let t in a.exports) {
                    let e = a.exports[t];
                    if (isFunction(e)) {
                        return fn(i, e);
                    }
                }
            });
        }
    }

    filter(fn) {
        let result = [];
        if (fn) {
            this.installed.scan((i, a) => {
                let r = fn(i, a.exports.__esModule ? a.exports.default : a.exports);
                (r !== undefined) && result.push(r);
            });
        }
        return result;
    }

    get(path) {
        let a = this.installed.get(path);
        if (a) {
            return a.exports.__esModule ? a.exports.default : a.exports;
        }
        return null;
    }

    has(path) {
        return this.installed.get(path) !== undefined;
    }

    set(path, _exports) {
        this.installed.set(path, { exports: _exports });
        return this;
    }

    uninstall(...args) {
        args.forEach(path => {
            this.installed.delete(path);
            this.moduleFnsLoaded.delete(path);
        });
    }

    getLoadedModules() {
        let r = {};
        this.moduleFnsLoaded.forEach((value, key) => {
            r[key] = value;
        });
        return r;
    }
}

class SourceManager {
    constructor(context) {
        this._context = context;
        this._queue = new Queue();
        let target = null;
        if (!context.isBrowser) {
            target = EmptyPersistence;
        } else {
            if (context.window.indexedDB || context.window.mozIndexedDB || context.window.webkitIndexedDB) {
                target = DatabasePersistence;
                target.ready();
            } else if (context.window.localStorage) {
                target = StoragePersistence;
            } else {
                target = EmptyPersistence;
            }
        }
        this.persistence = target;
    }

    get context() {
        return this._context;
    }

    _getSourceCode(path) {
        return this.context.request.origin({ url: path, method: 'get' }).promise.then(info => info.data);
    }

    _getSourceByHash(filepath, _, rhash, source) {
        let r = 0, option = this.context.config, _source = {};
        let file = Reflect.ownKeys(option.sourceMap.packages).filter(packetname => {
            return option.sourceMap.packages[packetname].indexOf(rhash) !== -1;
        })[0];
        if (file) {
            option.sourceMap.packages[file].split("|").forEach(key => {
                let name = Reflect.ownKeys(option.sourceMap).filter(name => option.sourceMap[name] === key)[0];
                let hash = option.sourceMap[name];
                if (!source[hash]) {
                    r += 1;
                }
            });
            if (r > 4) {
                return this._getSourceCode(`${this.getRealPath(file + ".js", true)}`).then(code => {
                    let info = JSON.parse(code.substring(code.indexOf(",") + 1, code.length - 1));
                    Reflect.ownKeys(info).forEach(name => {
                        let { hash, code } = info[name];
                        _source[hash] = code;
                    });
                    return this.persistence.saveAll(_source).then(() => {
                        return source[rhash];
                    });
                });
            } else {
                return this._getSourceCode(`${this.getRealPath(filepath)}`).then(code => {
                    _source[rhash] = code;
                    return this.persistence.saveAll(_source).then(() => code);
                });
            }
        } else {
            if (rhash) {
                return this._getSourceCode(`${this.getRealPath(filepath)}`).then(code => {
                    _source[rhash] = code;
                    return this.persistence.saveAll(_source).then(() => code);
                });
            } else {
                return this._getSourceCode(`${this.getRealPath(filepath)}`);
            }
        }
    }

    _getSource(filepath) {
        return this._getCache().then(source => {
            let path = getMappedPath(filepath), option = this.context.config;
            let rhash = option.sourceMap[path];
            if (source[rhash]) {
                return source[rhash];
            } else {
                if (option.sourceMap) {
                    return this._getSourceByHash(filepath, path, option.sourceMap[path], source);
                } else {
                    return this._getSourceCode(`${this.getRealPath(filepath)}`);
                }
            }
        });
    }

    _getCache() {
        return this.persistence.getAll();
    }

    getRealPath(path, ispackage = false) {
        let r = path, hash = "", option = this.context.config;
        if (ispackage) {
            hash = option.sourceMap[path.split(".").shift()];
        } else {
            hash = option.sourceMap[getMappedPath(path)];
        }
        if (!option.develop) {
            let a = path.split("/");
            let b = a.pop();
            let c = b.split(".");
            a.push(`${hash}.${c[1]}`);
            r = a.join("/");
        } else {
            r = `${path}?h=${new Date().getTime()}`;
        }
        return `${option.siteURL[option.siteURL.length - 1] === "/" ? option.siteURL : option.siteURL + "/"}${r}`;
    }

    decompress(source = {}) {
        const _source = {};
        Reflect.ownKeys(source || {}).forEach(name => {
            let { hash, code } = source[name];
            _source[hash] = code;
        });
        return this.persistence.saveAll(_source);
    }

    getManifest({ name, version }) {
        return this.persistence.getManifest(name, version);
    }

    setManifest(info) {
        let config = this.context.config;
        this.persistence.setManifest({
            [info.name]: {
                develop: config.develop,
                name: info.name,
                root: info.root,
                siteURL: info.siteURL,
                version: info.version,
                map: info.map
            }
        }).then(() => {
            return this.persistence.clean(config.apps);
        }).catch(e => {
            console.log(e);
        });
        return Promise.resolve();
    }

    get(path) {
        return this._queue.add(() => this._getSource(path));
    }

    isCached(path) {
        return this._getCache().then(source => {
            let _path = getMappedPath(path), option = this.context.config;
            return !!source[option.sourceMap[_path]];
        });
    }

    uncache(...args) {
        return this._getCache().then(source => {
            args.forEach(path => {
                let _path = getMappedPath(path), option = this.context.config;
                delete source[option.sourceMap[_path]];
            });
        });
    }
}

class Loader {
    constructor(context) {
        this._context = context;
        const sourceManager = this._sourceManager = new SourceManager(context);
        this._moduleManager = new ModuleManager(context, sourceManager);
    }

    get context() {
        return this._context;
    }

    get moduleManager() {
        return this._moduleManager;
    }

    get sourceManager() {
        return this._sourceManager;
    }

    setCommonModules(map) {
        Reflect.ownKeys(map).forEach(name => {
            CommonInstalled.set(name, map[name]);
        });
    }

    removeCommonModule(name) {
        CommonInstalled.delete(name);
    }

    getModule(path) {
        return this.moduleManager.load(path).then(_exports => {
            if (_exports.__esModule === true) {
                return _exports.default;
            } else {
                return _exports;
            }
        });
    }

    getSource(path) {
        return this.sourceManager.get(path);
    }

    decompress(source = {}) {
        return this.sourceManager.decompress(source);
    }

    isCached(path) {
        return this.sourceManager.isCached(path);
    }

    setManifest(info) {
        return this.sourceManager.setManifest(info);
    }

    getManifest({ name, version }) {
        return this.sourceManager.getManifest({ name, version });
    }

    getRemoteMainifest({ name, version }) {
        let url = '';
        if (this.context.config.develop) {
            url = `/${name}/manifest.json?t=${version}`;
        } else {
            url = `/${name}/${version}.json`;
        }
        let info = ManifestLoaderMap.get(url);
        if (!info) {
            ManifestLoaderMap.set(url, { ps: null, data: undefined });
            info = ManifestLoaderMap.get(url);
        }
        if (info.data !== undefined) {
            return Promise.resolve(info.data);
        } else {
            if (!info.ps) {
                info.ps = this.context.request.origin({ url, method: 'get' }).promise.then(a => {
                    info.data = JSON.parse(a.data);
                    return info.data;
                });
            }
            return info.ps;
        }
    }

    uninstallModules(names = []) {
        this.moduleManager.uninstall(...names);
    }

    uncacheSource(paths = []) {
        this.sourceManager.uncache(...paths);
    }
}

module.exports = Loader;